package Dungeon;

import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Set;

import be.kuleuven.cs.som.annotate.*;

import ConditionPackage.Condition;
import ConditionPackage.DefaultCondition;
import ConditionPackage.RockCondition;
import SquarePackage.Direction;
import SquarePackage.Square;

/** //TODO annotatie tags => zie boek. 
 * Class for a dungeon.
 * 
 * @invar	All coordinates are positive and smaller than the maximum value.
 * 			| isValidDimension()
 * @invar	Can only have E as type of square.
 *  
 * @author 	Brecht && Florian
 * @version	1.0
 */
public abstract class Dungeon<E extends Square> extends Subdungeon<E>{
	
	///////////////////////
	//////CONSTRUCTOR//////
	///////////////////////
	/**
	 * Initializes a new dungeon with the given coordinates for the maximum coordinate
	 * 
	 * @param	maxX
	 * 			The maximum x-coordinate value.
	 * @param	maxY
	 * 			The maximum y-coordinate value.
	 * @param	maxZ
	 * 			The maximum z-coordinate value. 			
	 * @effect	The given coordinates are the coordinates of the maximum dimension.
	 * 			| Dimension(maxX, maxY, maxZ) == new.getMaxDimension() 
	 */
	public Dungeon(long maxX,long maxY, long maxZ){
		super(maxX, maxY, maxZ);
		this.rooms = new HashSet<Room<E>>();
	}
	
	/**
	 * Initializes a new dungeon with the given dimension as maximum dimension.
	 * 
	 * @param 	maxDimension
	 * 			The maximum dimension
	 * @effect	The given maximum dimension is the new max dimension.
	 * 			| new.getMaxDimension = maxDimension
	 */
	public Dungeon(Dimension maxDimension){
		super(maxDimension);
		this.rooms = new HashSet<Room<E>>();
	}
	
	/**
	 * Initializes a default dungeon.
	 * 
	 * @effect	The dungeon is initialized with 20,20,20 as maximum dimension.
	 * 			| this(new Dimension(20,20,20))
	 */
	public Dungeon(){
		this(20,20,20);
	}
	
	/////////////////////
	//////VARIABLES//////
	/////////////////////
	/**
	 * Set registering the used dimensions.
	 */
	private Set<Dimension> usedDimensions = new HashSet<Dimension>();
	
	/**
	 * Map registering the coordinates of the squares and the squares.
	 */
	private Set<Room<E>> rooms;
	
	///////////////////
	//////GETTERS//////
	///////////////////	
	/**
	 * Return the used dimensions.
	 */
	@Basic
	public Set<Dimension> getUsedDimensions(){
		return usedDimensions; 
	}
	
	/**
	 * Return the set of rooms in this dungeon.
	 */
	@Basic
	public Set<Room<E>> getRooms(){
		return rooms;
	}

	/**
	 * Returns all the levels and shafts in this dungeon.
	 * 
	 * @note 	A list with as only element this dungeon.
	 */
	@Override
	public Collection<Dungeon<? extends E>> getAllLevelsAndShafts(){
		Set<Dungeon<? extends E>> levelsAndShafts = new HashSet<Dungeon<? extends E>>();
		levelsAndShafts.add(this);
		return levelsAndShafts;
	}
	
	/**
	 * Return the square on the given dimension.
	 * 
	 * @param	dimension
	 * 			The dimension to find the square.
	 * @return	The square at the given position. 
	 * 			The result could be null if the given position doesn't contain a square.
	 * 			| for each room in getRooms()
	 * 			| if(room.getSquareAt(dimension) != null)
	 * 			| result = room.getSquareAt(dimension)
	 */
	@Override
	public E getSquareAt(Dimension dimension){
		for(Room<E> room: getRooms()){
			E result = (E) room.getSquareAt(dimension);
			if(result != null)
				return result;
		}
		return null;
	}
	
	/**
	 * Returns the room which the given square contains.
	 * 
	 * @param 	square
	 * 			The square to know the room of.
	 * @return	The room which contains the given square.
	 * 			| for each room in getRooms()
	 * 			| if(room.contains(square)
	 * 			| result = room
	 */
	@Override
	public Room<E> getRoomOf(Square square){//TODO E?
		for(Room<E> room: getRooms()){
			if(room.contains(square))
				return room;
		}
		return null;
	}
	
	/**
	 * Returns the room which the given dimension contains.
	 * 
	 * @param 	dimension
	 * 			The dimension to know the room of.
	 * @return	The room which contains the given dimension.
	 * 			| for each room in getRooms()
	 * 			| if(room.contains(dimension))
	 * 			| result = room
	 */
	public Room<E> getRoomOf(Dimension dimension){
		for(Room<E> room: getRooms()){
			if(room.contains(dimension))
				return room;
		}
		return null;
	}
	
	/**
	 * Return the dimension of the given square.
	 * 
	 * @param 	square
	 * 			The square of which the dimension must be known.
	 * @return	The dimension of the given square.
	 * 			| for each room in getRooms()
	 * 			| if(room.contains(square)
	 * 			| result = room.getDimensionOf(square)
	 */
	@Override
	public Dimension getDimensionOf(E square){
		for(Room<E> room: getRooms()){
			Dimension dimension = room.getDimensionOf(square);
			if(dimension != null)
				return dimension;
		}
		return null;
	}
	
	/**
	 * Get the given dimension in the coordinate system of the root dungeon. 
	 * 
	 * @param 	dimension
	 * 			The dimension to be known in the root dungeon coordinate system.
	 * @return	The absolute dimension of the given dimension.
	 * 			| if(hasContainingDungeon())
	 * 			| result = getContainingDungeon().getRootdimension(dimension, this);
	 * 			| else result = dimension
	 */
	@Override
	public Dimension getRootDimension(Dimension dimension){
		if(!hasContainingDungeon())
			return dimension;
		return getContainingDungeon().getRootDimension(dimension.add(getStartDimensionInContainingdungeon()));
	}
	

	/**
	 * Returns a collection of squares that are in the same room as the given square.
	 * 
	 * @param	square
	 * 			A square in the room of which all the squares are wanted.
	 * @return	A collection of squares that are in the same room as the given square
	 * 			| result = getRoomOf(square).getAllDimensions()
	 */
	public Collection<Square> GetAllSquaresOfRoomOf(E square){
		Room<E> room = getRoomOf(square);
		if(room == null)
			return null;
		return room.getAllSquaresInWholeRoom();
	}
	
	//////////////////////////////////
	///////ADD AND REMOVE SQUARE//////
	//////////////////////////////////
	/**
	 * Add the given square on the given dimension.
	 * 
	 * @param 	square
	 * 			The square to add.
	 * @param 	dimension
	 * 			The dimension where the square should be added.
	 * @post	The square is added at the given dimension.
	 * 			| getAllSquares().contains(square)
	 * 			| getAllDimensions().contains(dimension)
	 * @throws 	IllegalArgumentException
	 * 			One of the arguments is not a valid argument.
	 * 			| !isValidDimension()||!isValidSquare()
	 */
	@Model @Override
	public void addSquare(E square, Dimension dimension)throws IllegalArgumentException{
		super.addSquare(square, dimension);
		rooms.add(new Room<E>(this, square, dimension));
		square.setStateUsed();
		usedDimensions.add(dimension);
		for(Direction direction: Direction.values()){
			Dimension otherDim = getRootDimension(dimension).getDimensionIn(direction);
			if(otherDim!=null){
			Square other = getRootDungeon().getSquareAt(otherDim);
			if(other!=null){
				setBorder(square, other, direction);
				if(!(square.getBorderAt(direction).getIsolation())){
					getRootDungeon().getRoomOf(other).mergeWith(getRootDungeon().getRoomOf(square));
				}

			}
			}
		}
		getRootDungeon().updateRocks();
		square.setStateUsed();
      	}
	
	/**
	 * Remove the given square at the given dimension.
	 * 
	 * @param	dimension
	 * 			The given dimension where the square must be removed.
	 * @post	The square at the given position isn't a part of the dungeon anymore.
	 * 		  	| getSquareAt(dimension)=null
	 * 			| getAllUsedDimensions.contains(dimension) = null
	 * @throws	The dimension is not a valid dimension.
	 * 			| !isValidDimension(dimension)
	 */
	@Model @Override
	public void removeSquareAt(Dimension dimension) throws IllegalArgumentException{
		if(!isValidRemovableDimension(dimension))
			throw new IllegalArgumentException(dimension + " is not a valid dimension.");
		Room<E> room = getRoomOf(dimension);
		Set<Square> neighbours = getSquareAt(dimension).getNeighbours();
		room.removeSquareAt(dimension);
		usedDimensions.remove(dimension);
		for(Square other: neighbours){
			if(room.contains(other)){ 
				Room<E> room2 = new Room<E>(this, room.getNeighbourSquares(room.getDimensionOf(other)));
				room2.updateArea();
				room.removeDoubleSquares(room2);
				rooms.add(room2);
			}
			else {
					getRootDungeon().getRoomOf(other).updateArea();
			}
				
		}
	}
	
	/**
	 * Removes this room if the squares of this room are used twice in this dungeon.
	 * 
	 * @param 	room
	 * 			The room to remove.
	 * @param	room2
	 * 			The room to keep.
	 * @post	If room contains squares that room2 contains too, those squares are removed, and so is room itself.
	 * 			| !getRooms().contains(room)
	 * @post 	Room is terminated.
	 * 			| room.isTerminated()
	 */
	public void removeDoubleRoom(Room<E> room, Room<E> room2){
		rooms.remove(room);
		room2.removeDoubleSquares(room);
		room.terminate();
	}
	
	
	////////////////
	//////DOOR//////
	////////////////
	/**
	 * Opens a door in the given square in the given direction.
	 * 
	 * @param 	square
	 * 			The square which contains a door.
	 * @param 	direction
	 * 			The direction in the given square where the door stands.
	 * @effect	The rooms on both sides of the door get merged.
	 * 			| room1.mergeWith(room2)
	 * @throws 	IllegalArgumentException
	 * 			The given square doesn't exist in this dungeon.
	 * 			| getDimensionOf(square) == null
	 * @throws 	IllegalArgumentException
	 * 			There is no door in the given direction or the square isn't part of this dungeon.
	 * 			| !isDoor(square, direction)
	 */
	@Model @Override
	public void openDoor(E square, Direction direction)throws IllegalArgumentException{
		Dimension dimension = getDimensionOf(square);
		if(dimension == null)
			throw new IllegalArgumentException("Not part of this dungeon");
		square.openDoor(direction);
		getRootDungeon().getRoomOf(square.getBorderAt(direction).getIdentifier()).mergeWith(getRootDungeon().getRoomOf(square.getBorderAt(direction).getSecondSquare()));
		getRootDungeon().updateRocks();
	}
	
	/**
	 * Closes a door in the given square in the given direction.
	 * 
	 * @param 	square
	 * 			The square which contains a door.
	 * @param 	direction
	 * 			The direction in the given square where the door stands.
	 * @post	The rooms on both sides of the door get separated.
	 * 			| new Room(room.getNeighbourSquares(getDimensionOf(square))
	 * 			| room2.removeDoubleSquares(room)
	 * @throws 	IllegalArgumentException
	 * 			The given square doesn't exist in this dungeon.
	 * 			| getDimensionOf(square) == null
	 * @throws 	IllegalArgumentException
	 * 			There is no door in the given direction or the square isn't part of this dungeon.
	 * 			| !isDoor(square, direction)
	 */
	@Model @Override
	public void closeDoor(E square, Direction direction)throws IllegalArgumentException{
		Dimension dimension = getDimensionOf(square);
		if(dimension == null)
			throw new IllegalArgumentException("Not part of this dungeon");
		square.closeDoor(direction);
		Room<E> room = getRoomOf(square);
		Room<E> room2 = new Room<E>(this, room.getNeighbourSquares(getDimensionOf(square)));
		room2.removeDoubleSquares(room);
		rooms.add(room2);
		room2.updateArea();
		room.updateArea();
	}
		
	////////////////////////////
	//////EXTRA FOR OUTPUT//////
	////////////////////////////
	/**
	 * Returns a string of this dungeon.
	 */
	@Override
	public String toString(){
		String returnString = "";
		for(Room<E> room : getRooms()){
			returnString += room.toString();
		}
		return returnString;
	}
	
	/////////////////////
	//////ITERATOR///////
	/////////////////////
	public Iterator<? extends E> iterator(final Condition filterCondition){
		return new Iterator<E>(){

            private Iterator<Room<E>> roomIterator = getRooms().iterator();
            private Iterator<E> squareIterator;
            private E nextSquare;
            private Condition filter;

            {
                    start();
            }
            
            private void start(){
            	if(roomIterator.hasNext())
            		squareIterator = roomIterator.next().getAllSquares().iterator();
            		if(filterCondition==null){
            			filter = new DefaultCondition();
            		}
            		filter =  filterCondition;
            }
            @Override
            public boolean hasNext() {
            	while(nextSquare== null && (squareIterator.hasNext()||roomIterator.hasNext())){
            		if(squareIterator.hasNext()){
            			E checkSquare = squareIterator.next();
            			if(filter.checkCondition(checkSquare)){
            				nextSquare=checkSquare;
            			}
            		}
            		else {    squareIterator = (Iterator<E>) roomIterator.next().getAllSquares().iterator();
                	         hasNext();
            		}
            	}
            	return nextSquare!=null;
            }
            @Override
            public E next() {
            	if(nextSquare == null){
            		hasNext();
            	}
            	E result = nextSquare;
            	nextSquare = null;
            	if(result==null) throw new NoSuchElementException();
            	return result;   
            }
            @Override
            public void remove() throws UnsupportedOperationException{
                    throw new UnsupportedOperationException();
            }
    };
	}
}